我觀看了 YouTube 視頻,其中 Angular 團隊負責人 Alex Rickabaugh 不鼓勵使用 effect
。 然後,他示範了一種用 computed signal
取代 effect
的方法,這種方法並不直觀,並且需要開發人員進行思維轉變,在 computed signal 中包含 WritableSignal
。
今天,我想用 signals
和 computed signals
來取代 explicitEffect
。
searchId = signal(initialId);
id = signal(initialId);
person = signal<undefined | Person>(undefined);
films = signal<string[]>([]);
rgb = signal('brown');
fontSize = computed(() => this.id() % 2 === 0 ? '1.25rem' : '1.75rem');
該元件有一些 signals 來儲存 searchId
、id
、person
、films
和 rgb
程式碼。 fontSize
computed signal 根據 id
得出字體大小。
#logIDsEffect = explicitEffect([this.searchId],
([searchId]) => console.log('id ->', this.id(), 'searchID ->', searchId), { defer: true });
#rgbEffect = explicitEffect([this.rgb], ([rgb]) => console.log('rgb ->', rgb), { defer: true });
constructor() {
explicitEffect([this.id], ([id], onCleanUp) => {
const sub = getPersonMovies(id, this.injector)
.subscribe((result) => {
if (result) {
const [person, ...rest] = result;
this.person.set(person);
this.films.set(rest);
} else {
this.person.set(undefined);
this.films.set([]);
}
this.rgb.set(generateRGBCode());
this.rgb.set(generateRGBCode());
this.rgb.set(generateRGBCode());
});
this.renderer.setProperty(this.hostElement, 'style', `--main-font-size: ${this.fontSize()}`);
if (id !== this.searchId()) {
this.searchId.set(id);
}
onCleanUp(() => sub.unsubscribe());
});
}
此元件具有三種 effect,可在控制台中記錄 signals 或更新 signals 。這些 signals 需要用 computed state 取代。
程式碼審查後,流程是保留 id
signal 並消除其餘 signals。 第一步是新增 computed state 並刪除 fontSize
computed signal。
state = computed(() => {
return {
fontSize: this.id() % 2 === 0 ? '1.25rem' : '1.75rem',
};
});
當 id
signal 更新時,state
computed signal 會為 fontSize
屬性匯出新的字體大小。
host: {
'[style.--main-font-size]': 'state().fontSize',
},
使用 host
屬性而不是 Renderer2
和 ElementRef
來更新 CSS 變數。
state = computed(() => {
return {
fontSize: this.id() % 2 === 0 ? '1.25rem' : '1.75rem',
rgb: generateRGBCode(),
};
});
當 id
signal 改變時, state
computed signal 會為 rgb
屬性匯出新的 RGB 值。同樣,host
屬性也會更新 CSS 變數,並刪除 #rgbEffect
effect,以便它不會記錄 rgb
變更。
host: {
'[style.--main-color]': 'state().rgb',
},
searchId
signal 比其他 signals 需要更多的工作。 當 id
signal 更新時,它也具有相同的值。 當 seachId
signal 發生變化時,id
signal 也接收到最新的值。
state = computed(() => {
const result = this.#personMovies();
return {
fontSize: this.id() % 2 === 0 ? '1.25rem' : '1.75rem',
rgb: generateRGBCode(),
searchId: signal(this.id()),
};
});
在 state
computed signal 中,searchId
屬性是一個初始值為 this.id()
的 signal。 當 id
signal 隨後發生變化時,state
computed signal 會同步 searchId
屬性的值。
syncId(id: number) {
if (id >= this.min && id <= this.max) {
this.state().searchId.set(id);
this.id.set(id);
}
}
當使用者在文字欄位中輸入新的 id 時,syncId
方法會設定 searchId
屬性和 id
signal。
<input type="number" [ngModel]="state().searchId()" (ngModelChange)="syncId($event)" />
輸入欄位不能使用雙向資料綁定將 searchId
signal 綁定到 ngModel
directive。 ngModelChange
event emitter 呼叫 syncId
方法來更新 signal。
在 constructor 中,刪除RxJS程式碼,因為它沒有被使用。
toObservable(this.searchId).pipe(
debounceTime(300),
distinctUntilChanged(),
filter((value) => value >= this.min && value <= this.max),
map((value) => Math.floor(value)),
takeUntilDestroyed(),
).subscribe((value) => this.id.set(value));
與 syncId
方法相比,我更喜歡上面的RxJS程式碼;我寧願使用 effect
來同步 id
和 searchId
signals 的值。
function getPersonMovies(http: HttpClient) {
return function(source: Observable<Person>) {
return source.pipe(
mergeMap((person) => {
const urls = person?.films ?? [];
const filmTitles$ = urls.map((url) => http.get<{ title: string }>(url).pipe(
map(({ title }) => title),
catchError((err) => {
console.error(err);
return of('');
})
));
return forkJoin([Promise.resolve(person), ...filmTitles$]);
}),
catchError((err) => {
console.error(err);
return of(undefined);
}));
}
}
這是一個自訂 RxJS 運算子,用於檢索星際大戰角色的詳細資訊和影片。
#personMovies = toSignal(toObservable(this.id)
.pipe(
switchMap((id) => this.http.get<Person>(`${URL}/${id}`)
.pipe(getPersonMovies(this.http))
),
), { initialValue: undefined });
#personMovies
使用 toSignal
和 toObservable 函數建立星際大戰詳細資訊的 signal。 我覺得toSignal(toObservable(this.id))
很長,對於初學者來說不容易理解。
state = computed(() => {
const result = this.#personMovies();
return {
fontSize: this.id() % 2 === 0 ? '1.25rem' : '1.75rem',
rgb: generateRGBCode(),
person: signal(result && result.length > 0 ? result[0] : undefined),
films: signal(result && result.length > 1 ? result.slice(1): []),
searchId: signal(this.id()),
};
});
如果 HTTP 請求成功,則定義 result
陣列。 person
屬性是一個 signal,其值是 result
的第一個元素。 films
屬性是一個 signal,其值是 result
剩餘的元素。
<div class="border">
@if(state().person(); as person) {
<p>Id: {{ id() }} </p>
<p>Name: {{ person.name }}</p>
<p>Height: {{ person.height }}</p>
<p>Mass: {{ person.mass }}</p>
<p>Hair Color: {{ person.hair_color }}</p>
<p>Skin Color: {{ person.skin_color }}</p>
<p>Eye Color: {{ person.eye_color }}</p>
<p>Gender: {{ person.gender }}</p>
} @else {
<p>No info</p>
}
<p style="text-decoration: underline">Movies</p>
@for(film of state().films(); track film) {
<ul style="padding-left: 1rem;">
<li>{{ film }}</li>
</ul>
} @empty {
<p>No movie</p>
}
</div>
HTML 範本根據 state
computed signal 顯示人物和影片。
結論:
effect
。signas-in-computed
,並在它們依賴的 signal 發生變化時更新屬性。toSignal
和 toObservable
發出 HTTP 請求。 toSignal(toObservable(this.id))
又長又難讀,我們可以查看 ngxtension
庫中的 toObservableSignal
函數。鐵人賽的第 31 天到此結束。
參考:
Techstack Nation Don't use effect: https://www.youtube.com/watch?v=aKxcIQMWSNU&feature=youtu.be
Stackblitz Demo: https://stackblitz.com/edit/stackblitz-starters-cejcoj?file=src%2Fstar-war%2Fstar-war.service.ts